package botlab.compiler;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Compile java source code from filepath or from CharSequence object.
 * Works with jar packages too. Use the eclipse JDT compiler.
 * @author Quentin Allender
 */
public class MyCompiler<T>
{

    private String workDir;
    private static PrintWriter errPrintWriter=new PrintWriter(System.err);

    /**
     * @param pw a {@link PrintWriter} where compilations errors will be written. 
     */
    public MyCompiler(PrintWriter pw)
    {
        if (pw != null)
            errPrintWriter = pw;
        
        try
        {
            setWorkDir();
        } catch (UnsupportedEncodingException ex)
        {
            Logger.getLogger(MyCompiler.class.getName()).log(Level.SEVERE, null, ex);
        } catch (Exception ex)
        {
            Logger.getLogger(MyCompiler.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
    
    /**
     * Set the errors output.
     * @param errPrintWriter a {@link PrintWriter}
     */
    public static void setErrPrintWriter(PrintWriter errPW)
    {
        errPrintWriter = errPW;
    }

    private void writeToFile(final CharSequence javaSource, String filePath)
    {
        FileOutputStream fos = null;

        try
        {
//            System.out.println("ecriture : "+filePath);
            File f = new File(filePath);
            f.createNewFile();
            fos = new FileOutputStream(filePath);
            BufferedOutputStream bos = new BufferedOutputStream(fos);
            PrintStream ps = new PrintStream(bos, true);

            ps.append(javaSource);
            ps.flush();

        } catch (IOException ex)
        {
            Logger.getLogger(MyCompiler.class.getName()).log(Level.SEVERE, null, ex);
        } finally
        {
            try
            {
                fos.close();
            } catch (IOException ex)
            {
                Logger.getLogger(MyCompiler.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }

    /** Compile source code given in {@link CharSequence} and designed to be written
     * in a file called <var>fileName</var>.
     * This file is actually created and source code is written to it. Then, in the same folder,
     * a .class file is generated. Finally, an instance of the class is returned.
     * @param javaSource the {@link CharSequence} to be compilled.
     * @param fileName the name of the java file, for instance : "MyClass.java".
     * @return an instance of the compilled class.
     */
    public T compile(final CharSequence javaSource, String fileName) throws CompilationException
    {
        // Default package name
        String packageName = "runtime";

        // Create the folder
        File f = new File(workDir + "/" + packageName);
        f.mkdir();

        String filePath = workDir + "/" + packageName + "/" + fileName;

        writeToFile(javaSource, filePath);
        return compile(filePath);
    }

    /** Compile the java source defined by the path <var>filePath</var>.
     *  A .class file is generated in the same folder as the source file.
     * @param filePath the path of the source file
     * @return an instance of the compiled class
     */
    public T compile(String filePath) throws CompilationException
    {
        // Eclipse compiler module
        if(!org.eclipse.jdt.internal.compiler.batch.Main.compile(
                new String[]
                {
                    filePath," -1.5"
                },
                new PrintWriter(System.out),
                errPrintWriter,
                null))
            throw new CompilationException();
        
        String classFilePath = filePath.replace(".java", ".class");
        File classFile = new File(classFilePath);
        String name = classFile.getName();
        name = name.replace(".class", ""); //without ".class"
        String packageName = classFile.getParentFile().getName();
//        System.out.println(name + "  " + packageName);
        try
        {
            return getInstance(packageName, name);
        } catch (ClassNotFoundException ex)
        {
            Logger.getLogger(MyCompiler.class.getName()).log(Level.SEVERE, null, ex);
        } catch (InstantiationException ex)
        {
            Logger.getLogger(MyCompiler.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IllegalAccessException ex)
        {
            Logger.getLogger(MyCompiler.class.getName()).log(Level.SEVERE, null, ex);
        }


        return null;
    }

    /**
     * Load a class given its <var>packageName</var> and <var>name</var>. 
     * The file must stand in the class path, for instance, the work directory 
     * of MyCompiler, i.e the user folder created by {@link MyCompiler#setWorkDir() }.
     * @param packageName the name of the package
     * @param name the name of the file without extension. ex : "MyClass"
     * @return the class
     * @throws ClassNotFoundException 
     */
    public Class<T> getClass(String packageName, String name) throws ClassNotFoundException
    {

        /*ClassLoader loader = ClassLoader.getSystemClassLoader();
        return (Class<T>) loader.loadClass(packageName + "." + name);
        return (Class<T>)Class.forName(packageName+"."+name);*/
        
        // we need a new instance of MyClassLoader each time we want to reload a class
        ClassLoader parentClassLoader = MyClassLoader.class.getClassLoader();
        MyClassLoader classLoader = new MyClassLoader(parentClassLoader);
        return (Class<T>) classLoader.loadClass(
                getFilePath(packageName,name+".class"),packageName+"."+name);
    }

    /**
     * Get an instance of the class returned by 
     * {@link MyCompiler#getClass(java.lang.String, java.lang.String)}.
     * @param packageName the package of the class
     * @param name its name without extension, ex : "MyClass"
     * @return an instance of the class
     * @throws ClassNotFoundException
     * @throws InstantiationException
     * @throws IllegalAccessException 
     */
    public T getInstance(String packageName, String name) throws ClassNotFoundException, InstantiationException, IllegalAccessException
    {
        return getClass(packageName, name).newInstance();
    }

    /**
     * Return the absolute filePath of a java source file given its name and
     * package. The file must stand in the work directory of MyCompiler, i.e the
     * user folder created by {@link MyCompiler#setWorkDir() }.
     * @param packageName the name of the package
     * @param name the name of the java file with extention ".java"
     * @return the String filePath
     */
    public String getFilePath(String packageName, String name)
    {
        return workDir + "/" + packageName + "/" + name;
    }

    /** Set the dir where temporary files will be stocked in order to be compiled.
     *  The name of the folder is "user" and this directory is added to the classPath.
     *  If the app is lauch via jar package, the new directory is created in the same folder as the .jar.
     *  If the app is launch via source code, the new directory is created in the build folder of the project
     */
    private void setWorkDir() throws UnsupportedEncodingException, Exception
    {
        String path = null;

        path = MyCompiler.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath();

        if (path == null)
        {
            throw new Exception("Local directory couldn't be find!");
        }
        path = URLDecoder.decode(path, "UTF-8");
        File f = new File(new File(path).getParent());
        workDir = f.getAbsolutePath() + "/user";
        File file = new File(workDir);
        file.mkdir();
        updateClassPath(workDir);
    }

    /**
     * Add a folder to the class path.
     * @param path the folder to be added
     */
    private void updateClassPath(String path)
    {
        try
        {
            ClassPathHacker.addFile(path);
        } catch (IOException ex)
        {
            Logger.getLogger(MyCompiler.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

}
